## GANET V2
## Copyright (c) 2014 Ganet Development Team. All rights reserved.
##
## This file is subject to the terms and conditions defined in file
## 'LICENSE.md', which is part of this source code package.
## See 'AUTHORS' file for a list of contributors.


import socket, select
from .peer import Peer

# Default values

_MIN_BUFFER_SIZE = 512    # minimum UDP buffers size in bytes
_DFLT_BUFFER_SIZE = 1024  # default UDP buffers size in bytes
_DFLT_TIMEOUT = 30.0      # default time in seconds
_DFLT_HEARTBEAT = 0.5     # default heartbeat in seconds
_DFLT_PORT = 54321

class NetworkListener(object):

    def on_new_connection(self, peer):
        pass

    def on_peer_lost(self, peer):
        pass

    def on_peer_disconnected(self, peer):
        pass

    def on_connection_request(self, peer):
        return True
    
    def on_connection_accepted(self, peer):
        pass

    def on_connection_refused(self, peer):
        pass

    def on_message(self, peer, msg):
        pass

    def on_error(self, error_num):
        pass

class Network(object):

    def __init__(self):
        self._buffer_size = _DFLT_BUFFER_SIZE
        self._timeout = _DFLT_TIMEOUT
        self._heartbeat = _DFLT_HEARTBEAT
        self._listeners = []
        self._peers = {}
        self._started = False
        self._read = []
        self._write = []
        self._sockets = []
        self._read = []
        self._write = []
        self._to_send = []

    #################### public configuration methods ####################

    @property
    def heartbeat(self):
        return self._heartbeat

    @heartbeat.setter
    def heartbeat(self, value):
        if value < 0 or value > self._timeout:
            raise ValueError('heartbeat must be between 0 and timeout')
        
        self._heartbeat = value
        for peer in self._peers.itervalues():
            peer._heartbeat = self._heartbeat

    @property
    def started(self):
        return self._started

    @property
    def timeout(self):
        return _timeout

    @timeout.setter
    def timeout(self, value):
        if value < 0 or value < self._heartbeat:
            err_txt = 'timeout must be greater than 0 and greater heartbeat'
            raise ValueError(err_txt)
        
        self._timeout = value
        for peer in self._peers.itervalues():
            peer._timeout = self._timeout

    @property
    def buffer_size(self):
        return self._buffersize

    @buffer_size.setter
    def buffer_size(self, value):
        if value < MIN_BUFFER_SIZE:
            err_txt = 'buffer size must be greater than or equal to '
            err_txt += MIN_BUFFER_SIZE
            raise ValueError(err_txt)
        
        self._buffersize = value

    ###################### public functional methods #####################

    def add_listener(self, listener):
        if listener in self._listeners:
            raise ValueError('network listener already added')
        self._listeners.append(listener)

    def remove_listener(self, listener):
        if listener not in self._listeners:
            raise ValueError('network listener not added')
        self._listeners.remove(listener)

    def startup(self, port = None):
        if self._started:
            raise ValueError('nerwork layer already started')

        if port:
            if (port <= 0 or port >= 65535):
                err_txt = 'port number must be between 1 and 65535'
                raise ValueError('port number must be between 1 and 65535')
        else:
            port = 0 # delegate to operating system (client mode)

        main_socket = socket.socket(type = socket.SOCK_DGRAM)
        main_socket.bind(('', port))
        self._sockets.append(main_socket)
        self._write.append(main_socket)
        self._read.append(main_socket)
        
    def shutdown(self):
        for peer in self._peers.itervalues():
            peer.disconnect()

        s = self._sockets[0]
        for msg in self._to_send:
            s.sendto(msg[0], msg[1])

        self._to_send[:] = []
        self._peers.clear()
            

        for s in self._sockets:
            s.close()

    def connect(self, addr):
        trans_addr = (socket.gethostbyname(addr[0]), addr[1])
        
        peer = Peer(self, trans_addr, self._heartbeat, self._timeout, False)
        self._peers[trans_addr] = peer
        peer._connect()

    def update(self, dt):
        # update peers
        for peer in self._peers.itervalues():
            peer._update(dt)

        # remove dead peers
        dead = [x[0] for x in self._peers.iteritems() if x[1].dead]
        for x in dead:
            self._peers.pop(x)

        # process sockets
        r, w, e = select.select(self._read, self._write, self._sockets, 0)
        while e or r or (w and self._to_send):
            self._process_exceptions(e)
            self._process_readable(r)
            self._process_writeable(w)
            r, w, e = select.select(self._read, self._write, self._sockets, 0)
    
    #################### private methods ####################

    def _log_info(self, msg):
        print '[info]: %s' % msg

    def _log_warning(self, msg):
        print '[warning]: %s' % msg

    def _log_debug(self, msg):
        #print '[debug]: %s' % msg
        pass
    
    def _process_message(self, msg, addr):
        peer = self._peers.get(addr, None)

        if not peer:
            peer =Peer(self, addr, self._heartbeat, self._timeout)
            self._peers[addr] = peer
        peer._process_message(msg)

    def _process_exceptions(self, e):
        for s in e:
            self._log_debug('got exception from socket')


    def _process_writeable(self, writeable):
        assert(len(writeable) <= 1)

        if not writeable or not self._to_send:
            # nothing to do
            return

        msg = self._to_send.pop(0)
        msg_data = msg[0]
        addr = msg[1]
        for s in writeable:
            s.sendto(msg_data, addr)

    def _process_readable(self, readable):
        try:
            for s in readable:
                msg = s.recvfrom(self._buffer_size)
                self._process_message(msg[0], msg[1])
        except socket.error as e:
            if e.errno == 10054:
                # on windows systems we get that error when local peer
                # has terminated
                pass
            else:
                log_txt = 'exception while processing readable sockets'
                log_txt += ' (%d): %s' % (e.errno, e.strerror)
                self._log_info(log_txt) 

    def _send(self, addr, msg):
        self._to_send.append((msg, addr))

